package arkham.net.messages;

import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.io.*;

import arkham.net.NetworkUtils;
import arkham.util.AHLogger;

public abstract class Message {
	
	/**
	 * This variable is only set on a receive call, and exists primarily
	 * for debugging purposes.
	 */
	protected int length;
	
	public static enum MessageType
	{
		TEXT_MESSAGE (6035, TextMessage.class),
		INFO_MESSAGE (5166, InfoMessage.class),
		FILE_MESSAGE (7965, FileMessage.class),
		COMMAND_MESSAGE (10162, CommandMessage.class),
		ERROR_MESSAGE (19718, ErrorMessage.class);
		
		private int id;
		private Class<? extends Message> msgClass;
		
		private MessageType(int id, Class<? extends Message> msgClass)
		{
			this.id = id;
			this.msgClass = msgClass;
		}
		
		public int id()
		{
			return id;
		}
		
		public Message getInstance()
		{
			try 
			{
				return msgClass.newInstance();
			} catch (InstantiationException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
			return null;
		}
		
		/**
		 * Gets the message type of this message
		 * @param id the id
		 * @return the message type
		 */
		public static MessageType getMessageType(int id)
		{
			for (MessageType mtype : MessageType.values())
			{
				if (mtype.id() == id)
				{
					return mtype;
				}
			}
			return null;
		}
	}
	
	public MessageType getType()
	{
		return MessageType.getMessageType(getId());
	}
	
	/**
	 * Converts this message into a byte array for transmission across
	 * a network
	 * @return the byte array
	 */
	public abstract byte[] toByteArray();
	
	/**
	 * Recreates its contents from byte array
	 * @param b the byte array
	 * @return the recreated message
	 */
	public abstract void fromByteArray(byte[] b) throws IOException;
	
	/**
	 * Gets the id of this message
	 * @return the id of the message
	 */
	public abstract int getId();
	
	/**
	 * Sends this message across a socket
	 * @param sock The socket to send across
	 * @return true if successfully sent, false otherwise
	 */
	public boolean send(Socket sock)
	{
		if (!sock.isConnected())
		{
			AHLogger.warning("Socket not connected: message not sent!");
			return false;
		}
		
		try
		{
			BufferedOutputStream os = new BufferedOutputStream(sock.getOutputStream());
			os.write(NetworkUtils.intToByteArray(getId()));
			byte[] b = this.toByteArray();
			os.write(NetworkUtils.intToByteArray(b.length));
			os.write(b);
			os.flush();
		}
		catch (IOException e)
		{
			AHLogger.warning("Failed to send message with id " + getId());
			AHLogger.warning("Message.send", e.toString());
			return false;
		}
		return true;
	}
	
	/**
	 * Receives this message from a socket
	 * @param s the socket to receive from
	 * @return the message received, or null if no message received
	 */
	public static Message receive(Socket s) throws IOException
	{
		try
		{
			byte[] smallbuf = new byte[8];
			InputStream is = s.getInputStream();
			
			int tbytes = 8;
			while (tbytes > 0)
			{
				int nbytes = is.read(smallbuf, 8 - tbytes, tbytes);
				if (nbytes == -1) continue;
				tbytes -= nbytes;
			}
			ByteBuffer bb = ByteBuffer.wrap(smallbuf);
			int msgid = NetworkUtils.byteArrayToInt(smallbuf, 0);
			int len = NetworkUtils.byteArrayToInt(smallbuf, 4);
			byte[] bigbuf = new byte[len];
			tbytes = len;
			while (tbytes > 0)
			{
				int nbytes = is.read(bigbuf, len - tbytes, tbytes);
				if (nbytes == -1) continue;
				tbytes -= nbytes;
			}
			MessageType msgType = MessageType.getMessageType(msgid);
			if (msgType == null)
			{
				AHLogger.warning("Message", "Received message with invalid id: " + msgid);
				return null;
			}
			Message msg = msgType.getInstance();
			msg.length = len;
			msg.fromByteArray(bigbuf);
			return msg;
		}
		catch (SocketTimeoutException e)
		{
			// don't warn for this
			throw e;
		}
		catch (IOException e)
		{
			AHLogger.warning("Message.receive", e.toString());
			throw e;
		}
		
	}
}
